package com.liuqianfei.markdown;

import java.util.LinkedList;

/**
 * 文本行的链表结构
 * 
 */
public class Line
{
    /**
     * 当前光标位置
     */
    public int position;
    /**
     * 行的开头和结尾的空格数
     */
    public int leading = 0, trailing = 0;
    /**
     * 标记当前行是否为空行
     */
    public boolean isEmpty = true;
    /**
     * 行的文本内容
     */
    public String value = null;
    /**
     * 前一行和后一行
     */
    public Line prev = null, next = null;
    /**
     * 标签当前行的前一行或后一行是否为空行
     */
    public boolean prevEmpty, nextEmpty;
    /**
     * XML块的最后一行
     */
    public Line xmlEndLine;

    public Line()
    {
    }

    /**
     * 计算当前行的开头和结尾的空格数。<br>
     * 如果当前行为空行，或只包含空格，设为空行。
     */
    public void init()
    {
        this.leading = 0;
        while (this.leading < this.value.length() && this.value.charAt(this.leading) == ' ')
        {
            this.leading++;
        }

        if (this.leading == this.value.length())
        {
            this.setEmpty();
        }
        else
        {
            this.isEmpty = false;
            this.trailing = 0;
            while (this.value.charAt(this.value.length() - this.trailing - 1) == ' ')
            {
                this.trailing++;
            }
        }
    }

    /**
     * 计算当前行开头的空格。
     */
    public void initLeading()
    {
        this.leading = 0;
        while (this.leading < this.value.length() && this.value.charAt(this.leading) == ' ')
        {
            this.leading++;
        }

        if (this.leading == this.value.length())
        {
            this.setEmpty();
        }
    }

    /**
     * 获取当前行的类型。
     *
     * @param configuration markdown解析器配置
     *
     * @return LineType
     */
    public LineType getLineType(final Configuration configuration)
    {
        // 空行
        if (this.isEmpty)
        {
            return LineType.EMPTY;
        }

        // 单行代码块
        if (this.leading > 3)
        {
            return LineType.CODE;
        }

        // 标题
        if (this.value.charAt(this.leading) == '#')
        {
            return LineType.HEADLINE;
        }

        // 引用
        if (this.value.charAt(this.leading) == '>')
        {
            return LineType.BLOCKQUOTE;
        }

        // 强制开启扩展，多行代码块
        if (configuration.forceExtendedProfile)
        {
            if (this.value.length() - this.leading - this.trailing > 2)
            {
                if (this.value.charAt(this.leading) == '`' && this.countCharsStartWith('`', configuration.allowSpacesInFencedDelimiters) >= 3)
                {
                    return LineType.FENCED_CODE;
                }
                if (this.value.charAt(this.leading) == '~' && this.countCharsStartWith('~', configuration.allowSpacesInFencedDelimiters) >= 3)
                {
                    return LineType.FENCED_CODE;
                }
            }
        }

        // 水平分割线
        if (this.value.length() - this.leading - this.trailing > 2 && (this.value.charAt(this.leading) == '*' || this.value.charAt(this.leading) == '-' || this.value.charAt(this.leading) == '_'))
        {
            if (this.countChars(this.value.charAt(this.leading)) >= 3)
            {
                return LineType.HR;
            }
        }

        // 无序列表
        if (this.value.length() - this.leading >= 2 && this.value.charAt(this.leading + 1) == ' ')
        {
            switch (this.value.charAt(this.leading))
            {
                case '*':
                case '-':
                case '+':
                    return LineType.UL;
            }
        }

        // 有序列表
        if (this.value.length() - this.leading >= 3 && Character.isDigit(this.value.charAt(this.leading)))
        {
            int i = this.leading + 1;
            while (i < this.value.length() && Character.isDigit(this.value.charAt(i)))
            {
                i++;
            }
            if (i + 1 < this.value.length() && this.value.charAt(i) == '.' && this.value.charAt(i + 1) == ' ')
            {
                return LineType.OL;
            }
        }

        // XML
        if (this.value.charAt(this.leading) == '<')
        {
            if (this.checkHTML())
            {
                return LineType.XML;
            }
        }

        // 当前行是否被作为标题
        if (this.next != null && !this.next.isEmpty)
        {
            if ((this.next.value.charAt(0) == '-') && (this.next.countChars('-') > 0))
            {
                return LineType.HEADLINE2;
            }
            if ((this.next.value.charAt(0) == '=') && (this.next.countChars('=') > 0))
            {
                return LineType.HEADLINE1;
            }
        }

        // 其他
        return LineType.OTHER;
    }

    /**
     * 读取XML注释。设置<code>xmlEndLine</code>。
     *
     * @param firstLine 开始读取的行
     * @param start 开始的光标位置
     * @return 返回新的光标位置，如果没有有效的注释，则返回-1。
     */
    private int readXMLComment(final Line firstLine, final int start)
    {
        Line line = firstLine;
        if (start + 3 < line.value.length())
        {
            if (line.value.charAt(2) == '-' && line.value.charAt(3) == '-')
            {
                int position = start + 4;
                while (line != null)
                {
                    while (position < line.value.length() && line.value.charAt(position) != '-')
                    {
                        position++;
                    }
                    if (position == line.value.length())
                    {
                        line = line.next;
                        position = 0;
                    }
                    else
                    {
                        if (position + 2 < line.value.length())
                        {
                            if (line.value.charAt(position + 1) == '-' && line.value.charAt(position + 2) == '>')
                            {
                                this.xmlEndLine = line;
                                return position + 3;
                            }
                        }
                        position++;
                    }
                }
            }
        }
        return -1;
    }

    /**
     * Checks if this line contains an ID at it's end and removes it from the
     * line.
     *
     * @return The ID or <code>null</code> if no valid ID exists.
     */
    // FIXME ... hack
    public String stripID()
    {
        if (this.isEmpty || this.value.charAt(this.value.length() - this.trailing - 1) != '}')
        {
            return null;
        }
        int p = this.leading;
        boolean found = false;
        while (p < this.value.length() && !found)
        {
            switch (this.value.charAt(p))
            {
                case '\\':
                    if (p + 1 < this.value.length())
                    {
                        switch (this.value.charAt(p + 1))
                        {
                            case '{':
                                p++;
                                break;
                        }
                    }
                    p++;
                    break;
                case '{':
                    found = true;
                    break;
                default:
                    p++;
                    break;
            }
        }

        if (found)
        {
            if (p + 1 < this.value.length() && this.value.charAt(p + 1) == '#')
            {
                final int start = p + 2;
                p = start;
                found = false;
                while (p < this.value.length() && !found)
                {
                    switch (this.value.charAt(p))
                    {
                        case '\\':
                            if (p + 1 < this.value.length())
                            {
                                switch (this.value.charAt(p + 1))
                                {
                                    case '}':
                                        p++;
                                        break;
                                }
                            }
                            p++;
                            break;
                        case '}':
                            found = true;
                            break;
                        default:
                            p++;
                            break;
                    }
                }
                if (found)
                {
                    final String id = this.value.substring(start, p).trim();
                    if (this.leading != 0)
                    {
                        this.value = this.value.substring(0, this.leading) + this.value.substring(this.leading, start - 2).trim();
                    }
                    else
                    {
                        this.value = this.value.substring(this.leading, start - 2).trim();
                    }
                    this.trailing = 0;
                    return id.length() > 0 ? id : null;
                }
            }
        }
        return null;
    }

    /**
     * 检查是否为有效的HTML块。同时设置<code>xmlEndLine</code>。
     *
     * @return 如果有效则返回<code>true</code>
     */
    private boolean checkHTML()
    {
        final LinkedList<String> tags = new LinkedList<String>();
        final StringBuilder temp = new StringBuilder();
        int position = this.leading;
        // fixed:一行只有一个 < 符号解析错误
        if ((this.value.length() > (this.leading + 1)) && this.value.charAt(this.leading + 1) == '!')
        {
            if (this.readXMLComment(this, this.leading) > 0)
            {
                return true;
            }
        }
        position = Utils.readXML(temp, this.value, this.leading, false);
        String element, tag;
        if (position > -1)
        {
            element = temp.toString();
            temp.setLength(0);
            Utils.getXMLTag(temp, element);
            tag = temp.toString().toLowerCase();
            if (!HTML.isHtmlBlockElement(tag))
            {
                return false;
            }
            if (tag.equals("hr") || element.endsWith("/>"))
            {
                this.xmlEndLine = this;
                return true;
            }
            tags.add(tag);

            Line line = this;
            while (line != null)
            {
                while (position < line.value.length() && line.value.charAt(position) != '<')
                {
                    position++;
                }
                if (position >= line.value.length())
                {
                    line = line.next;
                    position = 0;
                }
                else
                {
                    temp.setLength(0);
                    final int newPosition = Utils.readXML(temp, line.value, position, false);
                    if (newPosition > 0)
                    {
                        element = temp.toString();
                        temp.setLength(0);
                        Utils.getXMLTag(temp, element);
                        tag = temp.toString().toLowerCase();
                        if (HTML.isHtmlBlockElement(tag) && !tag.equals("hr") && !element.endsWith("/>"))
                        {
                            if (element.charAt(1) == '/')
                            {
                                if (!tags.getLast().equals(tag))
                                {
                                    return false;
                                }
                                tags.removeLast();
                            }
                            else
                            {
                                tags.addLast(tag);
                            }
                        }
                        if (tags.size() == 0)
                        {
                            this.xmlEndLine = line;
                            break;
                        }
                        position = newPosition;
                    }
                    else
                    {
                        position++;
                    }
                }
            }
            return tags.size() == 0;
        }
        return false;
    }

    /**
     * 标记当前行为空行。
     */
    public void setEmpty()
    {
        this.value = "";
        this.leading = this.trailing = 0;
        this.isEmpty = true;
        // 设置链表值
        if (this.prev != null)
        {
            this.prev.nextEmpty = true;
        }
        if (this.next != null)
        {
            this.next.prevEmpty = true;
        }
    }

    /**
     * 计算当前行中'ch'的数量。
     *
     * @param ch 被计数的字符
     * 
     * @return 如果当前行，只包含字符'ch'（可有空格），则返回一个大于0的值。
     */
    private int countChars(final char ch)
    {
        int count = 0;
        for (int i = 0; i < this.value.length(); i++)
        {
            final char c = this.value.charAt(i);
            if (c == ' ')
            {
                continue;
            }
            if (c == ch)
            {
                count++;
                continue;
            }
            count = 0;
            break;
        }
        return count;
    }

    /**
     * 计算当前行的开头字符'ch'的数量（可选择忽略空格）。
     *
     * @param ch 被计数的字符
     * @param allowSpaces 是否允许空格
     * 
     * @return 找到的字符数量
     */
    private int countCharsStartWith(final char ch, final boolean allowSpaces)
    {
        int count = 0;
        for (int i = 0; i < this.value.length(); i++)
        {
            final char c = this.value.charAt(i);
            if (c == ' ' && allowSpaces)
            {
                continue;
            }
            if (c == ch)
            {
                count++;
            }
            else
            {
                break;
            }
        }
        return count;
    }
    
    /**
     * Reads chars from this line until any 'end' char is reached.
     *
     * @param end
     *            Delimiting character(s)
     * @return The read String or <code>null</code> if no 'end' char was
     *         reached.
     */
    // TODO use Util#readUntil
    public String readUntil(final char... end)
    {
        final StringBuilder sb = new StringBuilder();
        int position = this.position;
        while (position < this.value.length())
        {
            final char ch = this.value.charAt(position);
            if (ch == '\\' && position + 1 < this.value.length())
            {
                final char c;
                switch (c = this.value.charAt(position + 1))
                {
                case '\\':
                case '[':
                case ']':
                case '(':
                case ')':
                case '{':
                case '}':
                case '#':
                case '"':
                case '\'':
                case '.':
                case '>':
                case '*':
                case '+':
                case '-':
                case '_':
                case '!':
                case '`':
                case '~':
                    sb.append(c);
                    position++;
                    break;
                default:
                    sb.append(ch);
                    break;
                }
            }
            else
            {
                boolean endReached = false;
                for (int n = 0; n < end.length; n++)
                {
                    if (ch == end[n])
                    {
                        endReached = true;
                        break;
                    }
                }
                if (endReached)
                {
                    break;
                }
                sb.append(ch);
            }
            position++;
        }

        final char ch = position < this.value.length() ? this.value.charAt(position) : '\n';
        for (int n = 0; n < end.length; n++)
        {
            if (ch == end[n])
            {
                this.position = position;
                return sb.toString();
            }
        }
        return null;
    }
    
    /**
     * Skips spaces.
     *
     * @return <code>false</code> if end of line is reached
     */
    // TODO use Util#skipSpaces
    public boolean skipSpaces()
    {
        while (this.position < this.value.length() && this.value.charAt(this.position) == ' ')
        {
            this.position++;
        }
        return this.position < this.value.length();
    }
}
